/*
 * https://github.com/vercel/ms
 */

// Helpers.
const s = 1000;
const m = s * 60;
const h = m * 60;
const d = h * 24;
const w = d * 7;
const y = d * 365.25;

type Unit =
	| "Years"
	| "Year"
	| "Yrs"
	| "Yr"
	| "Y"
	| "Weeks"
	| "Week"
	| "W"
	| "Days"
	| "Day"
	| "D"
	| "Hours"
	| "Hour"
	| "Hrs"
	| "Hr"
	| "H"
	| "Minutes"
	| "Minute"
	| "Mins"
	| "Min"
	| "M"
	| "Seconds"
	| "Second"
	| "Secs"
	| "Sec"
	| "s"
	| "Milliseconds"
	| "Millisecond"
	| "Msecs"
	| "Msec"
	| "Ms";

type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;

export type StringValue =
	| `${number}`
	| `${number}${UnitAnyCase}`
	| `${number} ${UnitAnyCase}`;

interface Options {
	/**
	 * Set to `true` to use verbose formatting. Defaults to `false`.
	 */
	long?: boolean;
}

/**
 * Parse or format the given value.
 *
 * @param value - The string or number to convert
 * @param options - Options for the conversion
 * @throws Error if `value` is not a non-empty string or a number
 */
export function ms(value: StringValue, options?: Options): number;
export function ms(value: number, options?: Options): string;
export function ms(
	value: StringValue | number,
	options?: Options,
): number | string {
	if (typeof value === "string") {
		return parse(value);
	}

	if (typeof value === "number") {
		return format(value, options);
	}

	throw new Error(
		`Value provided to ms() must be a string or number. value=${JSON.stringify(value)}`,
	);
}

/**
 * Parse the given string and return milliseconds.
 *
 * @param str - A string to parse to milliseconds
 * @returns The parsed value in milliseconds, or `NaN` if the string can't be
 * parsed
 */
export function parse(str: string): number {
	if (typeof str !== "string" || str.length === 0 || str.length > 100) {
		throw new Error(
			`Value provided to ms.parse() must be a string with length between 1 and 99. value=${JSON.stringify(str)}`,
		);
	}
	const match =
		/^(?<value>-?(?:\d+)?\.?\d+) *(?<unit>milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
			str,
		);

	if (!match?.groups) {
		return Number.NaN;
	}

	// Named capture groups need to be manually typed today.
	// https://github.com/microsoft/TypeScript/issues/32098
	const { value, unit = "ms" } = match.groups as {
		value: string;
		unit: string | undefined;
	};

	const n = Number.parseFloat(value);

	const matchUnit = unit.toLowerCase() as Lowercase<Unit>;

	/* istanbul ignore next - istanbul doesn't understand, but thankfully the TypeScript the exhaustiveness check in the default case keeps us type safe here */
	switch (matchUnit) {
		case "years":
		case "year":
		case "yrs":
		case "yr":
		case "y":
			return n * y;
		case "weeks":
		case "week":
		case "w":
			return n * w;
		case "days":
		case "day":
		case "d":
			return n * d;
		case "hours":
		case "hour":
		case "hrs":
		case "hr":
		case "h":
			return n * h;
		case "minutes":
		case "minute":
		case "mins":
		case "min":
		case "m":
			return n * m;
		case "seconds":
		case "second":
		case "secs":
		case "sec":
		case "s":
			return n * s;
		case "milliseconds":
		case "millisecond":
		case "msecs":
		case "msec":
		case "ms":
			return n;
		default:
			matchUnit satisfies never;
			throw new Error(
				`Unknown unit "${matchUnit}" provided to ms.parse(). value=${JSON.stringify(str)}`,
			);
	}
}

/**
 * Parse the given StringValue and return milliseconds.
 *
 * @param value - A typesafe StringValue to parse to milliseconds
 * @returns The parsed value in milliseconds, or `NaN` if the string can't be
 * parsed
 */
export function parseStrict(value: StringValue): number {
	return parse(value);
}

/**
 * Short format for `ms`.
 */
function fmtShort(ms: number): StringValue {
	const msAbs = Math.abs(ms);
	if (msAbs >= d) {
		return `${Math.round(ms / d)}d`;
	}
	if (msAbs >= h) {
		return `${Math.round(ms / h)}h`;
	}
	if (msAbs >= m) {
		return `${Math.round(ms / m)}m`;
	}
	if (msAbs >= s) {
		return `${Math.round(ms / s)}s`;
	}
	return `${ms}ms`;
}

/**
 * Long format for `ms`.
 */
function fmtLong(ms: number): StringValue {
	const msAbs = Math.abs(ms);
	if (msAbs >= d) {
		return plural(ms, msAbs, d, "day");
	}
	if (msAbs >= h) {
		return plural(ms, msAbs, h, "hour");
	}
	if (msAbs >= m) {
		return plural(ms, msAbs, m, "minute");
	}
	if (msAbs >= s) {
		return plural(ms, msAbs, s, "second");
	}
	return `${ms} ms`;
}

/**
 * Format the given integer as a string.
 *
 * @param ms - milliseconds
 * @param options - Options for the conversion
 * @returns The formatted string
 */
export function format(ms: number, options?: Options): string {
	if (typeof ms !== "number" || !Number.isFinite(ms)) {
		throw new Error("Value provided to ms.format() must be of type number.");
	}
	return options?.long ? fmtLong(ms) : fmtShort(ms);
}

/**
 * Pluralization helper.
 */
function plural(
	ms: number,
	msAbs: number,
	n: number,
	name: string,
): StringValue {
	const isPlural = msAbs >= n * 1.5;
	return `${Math.round(ms / n)} ${name}${isPlural ? "s" : ""}` as StringValue;
}
